package viper.main;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import viper.GUI.ViperGUI;
import viper.receiver.CallReceiver;
import viper.receiver.TCPReceiver;
import viper.receiver.UDPReceiver;
import viper.sender.CallSender;
import viper.sender.UDPSender;

/**
 * @author Kevin Sin
 * 
 * Revision number: 3
 * Date created: 3/21/10
 * Date of Last Back-up on SVN: 3/21/10
 * 
 * <p>This class is the central class that keeps track of what a user is doing
 * - logged off, in a call, setting up a call, who the callee is...
*
 *  Revision List:
 * 		- Added TCP packet handling in order to not miss packets
 * 		- Revising Author: Ji Kim
 * 		- Date Implemented: 04/02/10
 *
 */

public class StateManager {
	//-----------------Singleton structure-----------------------------------
	public final static StateManager INSTANCE = new StateManager();
	private StateManager(){
		
	}
	
	public void initialize(){
		//Initialize the UDP Socket
		try{
			datagramSocket = new DatagramSocket(VIPER_PORT);
		} catch (SocketException e) {
			//already an instance up so exit
			System.out.println("Already an instance running - not starting a new one");
			System.exit(1);
		}
		tcpsock = null;
		dos = null;
		dis = null;
		udpreceiver = null;
		tcpreceiver = null;
		
		/**
		//ringtone player
		ringPlayer = null;
		ringThread = null;
		try{
			clip = AudioSystem.getClip();
		}catch(LineUnavailableException lue){
			System.out.println("LUE 1");
		}
		File ringFile = new File(location);
		try{
			ais = AudioSystem.getAudioInputStream(ringFile);
		}catch(UnsupportedAudioFileException uafe){
			System.out.println("UAFE");
		}catch(IOException ioe){
			System.out.println("IOE 1");
		}
		try{
			clip.open(ais);
		}catch(LineUnavailableException lue){
			System.out.println("LUE 2");
		}catch(IOException ioe){
			System.out.println("IOE 2");
		}
		
		//ringtone player
		try{
			clip = Applet.newAudioClip(new URL(ringLocation));
		}catch(MalformedURLException mue){
			System.out.println("MalformedURLException trying to create ringtone player.");
		}
		//something else
		try{
			ringInput = new FileInputStream(ringLocation);
		}catch(FileNotFoundException fnfe){
			System.out.println("Ringtone not found!");
		}
		try{
			ringStream = new AudioStream(ringInput);
			ringData = ringStream.getData();
		}catch(IOException ioe){
			System.out.println("IOException trying to create ringtone stream.");
		}
		contRingStream = new ContinuousAudioDataStream(ringData);
		**/
	}
	//------------------------------------------------------------------------
	//----------------Represent state of the user-----------------------------
	public enum State {
		LOGGED_OUT, NOT_IN_CALL, DIALING, CALL_DECISION, IN_CALL 
	}
	private State state = State.LOGGED_OUT;
	
	private void setState(State state) {
		this.state = state;
	}

	public State getState() {
		return state;
	}
	
	//----------------------------------------------------------------
	//------------------------thread pool------------------------------
	private ExecutorService pool;
	
	/**
	 * @return pool
	 */
	public ExecutorService getPool() {
		return pool;
	}
	//----------------------------------------------------------------
	//------------------UDPSocket for receiving / sending------------
	private DatagramSocket datagramSocket; //UDP Socket
	private UDPReceiver udpreceiver;
	
	/**
	 * @return datagramSocket
	 */
	public DatagramSocket getDatagramSocket() {
		return datagramSocket;
	}
	
	public static final int VIPER_PORT = 40052; //UDP port to use
	//-----------------------------------------------------------------
	//-----------------TCP Stream Objects for Setup/End----------------
	private Socket tcpsock;
	private DataOutputStream dos;
	private DataInputStream dis;
	private TCPReceiver tcpreceiver;
	
	public static final int TCP_PORT = 40053; //TCP port to use
	
	public DataInputStream getInputStream(){
		return dis;
	}
	
	//-----------------------------------------------------------------
	//------------------------ringtone variables-----------------------
	/**
	private String ringLocation = "file:\\C:\\Users\\Hirameki\\workspace\\Viper_Fiercer\\src\\viper\\main\\ring2.mp3";
	private InputStream ringInput;
	private AudioStream ringStream;
	private AudioData ringData;
	private ContinuousAudioDataStream contRingStream;
	private AudioClip clip;
	private Clip clip = null;
	private AudioInputStream ais = null;
	**/
	private Thread ringThread;
	private RingtonePlayer ringPlayer;
	
	//-----------------------------------------------------------------
	//-----------------helper methods for IP addresses-----------------
	/**
	 * @return - IP address in String format
	 */
	public String getIP() {
		String ip = null;
		try {
			InetAddress address = InetAddress.getLocalHost();
			ip = address.getHostAddress();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		return ip;
	}
	//------------------------------------------------------------------
	//-------other private variables and related methods----------------
	private String username = null; //name that is displayed
	private ArrayList<Object[]> peers = new ArrayList<Object[]>(); //peers online
	private ArrayList<Object[]> relays = new ArrayList<Object[]>(); //relays used in a call
	private Object[] calleeArray = null; //Object[] for callee	
	private byte diversityCode = 0;
	
	private CallSender csender;
	private CallReceiver creceiver;

	public CallSender getCallSender(){
		return csender;
	}
	
	public CallReceiver getCallReceiver(){
		return creceiver;
	}
	
	/**
	 * @return
	 * Getter method for username
	 */
	public String getUsername() {
		return username;
	}
	
	/**
	 * @param username
	 * Setter method for username
	 */
	public void setUsername(String username) {
		this.username = username;
	}
	
	/**
	 * @return
	 * Getter method for peers
	 */
	public ArrayList<Object[]> getPeers() {
		return peers;
	}

	/**
	 * updates the peers variable
	 * make sure you are not added yourself
	 */
	public void updatePeers() {
		ArrayList<Object[]> users = MySQLFunctions.getCurrentUsers();
		for(Object[] userArray : users)
    	{
			if(((String)userArray[0]).equals(getIP()))
			{
				users.remove(userArray);
				break;
			}
    	}
		peers = users;
	}	
	
	public Object[] getPeerRowFromIP(String ip) {
		updatePeers();
		for(Object[] row : peers)
		{
			if(((String)row[0]).equals(ip))
				return row;
		}
		return null;
	}
	
	/**
	 * @return
	 * Getter method for relays
	 */
	public ArrayList<Object[]> getRelays() {
		return relays;
	}

	/**
	 * @param
	 * Setter method for relays
	 */
	public void setRelays(ArrayList<Object[]> relays) {
		this.relays = relays;
	}

	/**
	 * @return
	 * Getter method for calleeArray
	 */
	public Object[] getCalleeArray() {
		return calleeArray;
	}

	/**
	 * @param calleeArray
	 * Setter method for calleeArray
	 */
	public void setCalleeArray(Object[] calleeArray) {
		this.calleeArray = calleeArray;
	}
	
	public byte getDiversityCode() {
		return diversityCode;
	}

	public void setDiversityCode(byte diversityCode) {
		this.diversityCode = diversityCode;
	}

	//------------------------------------------------------------
	//--------------------State transitions-----------------------
	/**
	 * @param username
	 * Transition from LOGGED_OUT to NOT_IN_CALL
	 */
	public void login(String username){
		setState(State.NOT_IN_CALL);
		
		if(tcpreceiver==null && udpreceiver==null){
			pool = Executors.newCachedThreadPool();
			udpreceiver = new UDPReceiver();
			pool.execute(udpreceiver); //start UDPReceiver
			tcpreceiver = new TCPReceiver(null);
			pool.execute(tcpreceiver); //start TCPReceiver
		}
		
		setUsername(username);
		MySQLFunctions.login(getIP(), username);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		
	}
	
	/**
	 * Transitions from NOT_IN_CALL to LOGGED_OUT
	 */
	public void logout(){
		MySQLFunctions.logout(getIP());
		setState(State.LOGGED_OUT);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_OUT_CARD);
	}
	
	/**
	 * @param peerIP which peer the user wants to talk to
	 * Transition when setting up a call after picking somebody to call.
	 */
	public void setupCall(String peerIP){
		setCalleeArray(getPeerRowFromIP(peerIP));
		ViperGUI.INSTANCE.changeCard(ViperGUI.CALL_SENDER_SETUP_CARD);
	}

	/**
	 * @param sourceAddrIP
	 * @param typeCode
	 * @param relays
	 * 
	 * State transition from NOT_IN_CALL to CALL_DECISION
	 */
	public void gotCall(final String sourceAddrIP, final byte typeCode, final String[] relays) {
		//play ringtone
		ringPlayer = new RingtonePlayer();
		ringThread = new Thread(ringPlayer);
		ringThread.start();
		
		if(state==State.IN_CALL)
		{
			rejectCall(sourceAddrIP, typeCode, relays);
			return;
		}
		setState(State.CALL_DECISION);

		ViperGUI.INSTANCE.changeInternalFrameCallDecision(sourceAddrIP, typeCode, relays);
	}
	
	/**
	 * State transition from CALL_DECISION to NOT_IN_CALL
	 */
	public void callCancelled()
	{
		//stop ringtone
		ringPlayer.end();
		
		if(state!=State.CALL_DECISION)
			return;
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
	}
	
	/**
	 * State transition from CALL_DECISION to NOT_IN_CALL
	 */
	public void rejectCall(String sourceAddrIP, byte typeCode, String[] relays) {
		//stop ringtone
		ringPlayer.end();
		
		//send rejection packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_REJECT_CALL, 
				null);
		//byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		//UDPSender.sendUDP(addr, bytes);
		try{
			tcpreceiver.getDataOutputStream().write(bytes,0,bytes.length);
		}catch(Exception ioe){
			System.out.println("IOException in StateManager.rejectCall().");
		}
		
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
	}

	/**
	 * State transition from CALL_DECISION to IN_CALL.
	 * Accept call, send OK packet back
	 */
	public void acceptCall(String sourceAddrIP, byte typeCode, String[] relayIPs){
		//stop ringtone
		ringPlayer.end();
		
		//send OK packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_ACCEPT_CALL, 
				null);
		//byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		//UDPSender.sendUDP(addr, bytes);
		try{
			tcpreceiver.getDataOutputStream().write(bytes,0,bytes.length);
		}catch(Exception ioe){
			System.out.println("IOException in StateManager.acceptCall().");
		}
		
		//store typecode and relays and who is calling
		setDiversityCode(typeCode);
		ArrayList<Object[]> callRelays = new ArrayList<Object[]>();
		if(relayIPs!=null)
		{
			for (String relayIP : relayIPs)
			{
				callRelays.add(getPeerRowFromIP(relayIP));
			}
		}
		setRelays(callRelays);
		setCalleeArray(getPeerRowFromIP(sourceAddrIP));

		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		
		creceiver = new CallReceiver();
		pool.execute(creceiver);
		
		csender = new CallSender();
		pool.execute(csender);
		
		
		ViperGUI.INSTANCE.changeCard(ViperGUI.IN_CALL_CARD);
		setState(State.IN_CALL);
	}
	
	/**
	 * State transition from NOT_IN_CALL to DIALING
	 */
	public void call(){
		setState(State.DIALING);
		
		//play ringtone
		ringPlayer = new RingtonePlayer();
		ringThread = new Thread(ringPlayer);
		ringThread.start();
		
		//Close server socket, stop listening
		tcpreceiver.end();
		tcpreceiver.close();
		
		//wait until old socket and streams are closed and cleaned up
		while(!tcpreceiver.isClosed()){}
		
		//Initialize the TCP Socket/Streams
		byte[] calleeAddrBytes = UDPSender.ipStringToBytes((String)calleeArray[0]);
		InetAddress destIP = null;
		try {
			destIP = InetAddress.getByAddress(calleeAddrBytes);
			tcpsock = new Socket(destIP, TCP_PORT);
			dos = new DataOutputStream(tcpsock.getOutputStream());
			dis = new DataInputStream(tcpsock.getInputStream());
		}catch (UnknownHostException e) {
			System.out.println("UnknownHostException in StateManager.call().");
		}catch(IOException ioe){
			System.out.println("IOException in StateManager.call().");
		}
		System.out.println("Initialized socket and streams as client requsting call.");
		tcpreceiver = new TCPReceiver(dis);
		pool.execute(tcpreceiver); //start listening on the newly created socket instead
		
		//build setup packet
		byte[] payload = new byte[4*relays.size() + 1];
		payload[0] = diversityCode;
		int i = 0;
		for(Object[] relay : relays)
		{
			String ipString = (String)relay[0];
			byte[] ipBytes = UDPSender.ipStringToBytes(ipString);
			System.arraycopy(ipBytes, 0, payload, 4*i + 1, 4);
			i++;
		}
		
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_MAKE_CALL, 
				payload
		);
		//byte[] calleeAddrBytes = UDPSender.ipStringToBytes((String)calleeArray[0]);
		//UDPSender.sendUDP(calleeAddrBytes, bytes);
		
		//send setup packet via TCP stream
		try{
			dos.write(bytes,0,bytes.length);
		}catch(Exception ioe){
			System.out.println("IOException in StateManager.call().");
		}
		System.out.println("Sent call request.");

		ViperGUI.INSTANCE.changeInternalFrameDialing();
	}
	
	/**
	 * State transition from DIALING to NOT_IN_CALL
	 */
	public void cancelCall(String sourceAddrIP){
		//stop ringtone
		ringPlayer.end();
		
		//send cancel call packet back
		byte[] bytes = ViperHeader.makeCallSetupPacket(
				ViperHeader.SETUP_CANCEL_CALL, 
				null);
		//byte[] addr = UDPSender.ipStringToBytes(sourceAddrIP);
		//UDPSender.sendUDP(addr, bytes);
		try{
			dos.write(bytes,0,bytes.length);
		}catch(Exception ioe){
			System.out.println("IOException in StateManager.cancelCall().");
		}
		
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
	}
	
	/**
	 * State transition from DIALING to IN_CALL
	 */
	public void callAccepted() {
		//stop ringtone
		ringPlayer.end();
		
		if(state!=State.DIALING)
			return;
		
		creceiver = new CallReceiver();
		pool.execute(creceiver);
		
		csender = new CallSender();
		pool.execute(csender);
		
		ViperGUI.INSTANCE.changeCard(ViperGUI.IN_CALL_CARD);
		setState(State.IN_CALL);
	}
	
	/**
	 * State transition from DIALING to NOT_IN_CALL
	 */
	public void callRejected() {
		//stop ringtone
		ringPlayer.end();
		
		if(state!=State.DIALING)
			return;
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.getInternalFrame().setVisible(false);
	}
	
	//generic transmit method for sending data over TCP, auto-detects client/server type
	public void sendTCPPacket(byte[] pkt){
		if(tcpsock==null){
			try{
				tcpreceiver.getDataOutputStream().write(pkt,0,pkt.length);
			}catch(Exception ioe){
				System.out.println("IOException in StateManager.sendTCPPacket().");
			}
		}
		else{
			try{
				dos.write(pkt,0,pkt.length);
			}catch(Exception ioe){
				System.out.println("IOException in StateManager.sendTCPPacket().");
			}
		}
	}
	
	/**
	 * Transition from IN_CALL to NOT_IN_CALL
	 */
	public void endCall(){
		//only start closing streams/socket right away if this is server-side 
		if(tcpsock==null){ //if tcpsock was never initialized, then we know this was the receiver/server end of the call
			sendClientShutdown();
			callEnded();
		}
		//if client-side, request server to shut down first
		else{
			//send end call packet back
			byte[] bytes = ViperHeader.makeCallSetupPacket(
					ViperHeader.SETUP_END_CALL, 
					null);
			//byte[] addr = UDPSender.ipStringToBytes((String)calleeArray[0]);
			//UDPSender.sendUDP(addr, bytes);
			/** unnecessary **
			if(tcpsock==null){ //if tcpsock was never initialized, then we know this was the receiver/server end of the call
				try{
					tcpreceiver.getDataOutputStream().write(bytes,0,bytes.length);
				}catch(Exception ioe){
					System.out.println("IOException in StateManager.endCall().");
				}
			}
			**              **/
			//else{ //otherwise, we know this was the call initiator
				try{
					dos.write(bytes,0,bytes.length);
				}catch(Exception ioe){
					System.out.println("IOException in StateManager.endCall().");
				}
			//}
		}		
	
	}
	
	
	public void sendClientShutdown(){
		//if server-side we want to let the client know we're closing and close server side first
		if(tcpsock==null){
			//send end call packet back
			byte[] bytes = ViperHeader.makeCallSetupPacket(
					ViperHeader.SETUP_END_CALL, 
					null);
			//byte[] addr = UDPSender.ipStringToBytes((String)calleeArray[0]);
			//UDPSender.sendUDP(addr, bytes);
			try{
				tcpreceiver.getDataOutputStream().write(bytes,0,bytes.length);
			}catch(Exception ioe){
				System.out.println("IOException in StateManager.callEnded().");
			}
		}
	}
	
	/**
	 * Used in transition from IN_CALL to NOT_IN_CALL
	 */
	public void callEnded(){
		
		/**
		//if server-side we want to let the client know we're closing and close server side first
		if(tcpsock==null){
			//send end call packet back
			byte[] bytes = ViperHeader.makeCallSetupPacket(
					ViperHeader.SETUP_END_CALL, 
					null);
			//byte[] addr = UDPSender.ipStringToBytes((String)calleeArray[0]);
			//UDPSender.sendUDP(addr, bytes);
			try{
				tcpreceiver.getDataOutputStream().write(bytes,0,bytes.length);
			}catch(Exception ioe){
				System.out.println("IOException in StateManager.callEnded().");
			}
		}
		**/
		
		//close all instances and network objects
		if(state!=State.IN_CALL)
			return;
		csender.end();
		creceiver.end();
		setState(State.NOT_IN_CALL);
		ViperGUI.INSTANCE.changeCard(ViperGUI.LOGGED_IN_CARD);
		
		/*
		try{
			Thread.sleep(2000);
		}catch(InterruptedException ie){
			System.out.println("System end wait failed.");
		}
		*/
		
		tcpreceiver.end();
		//tcpreceiver.close();
		udpreceiver.end();
		try{
			if(dos!=null)
				dos.close();
			if(dis!=null)
				dis.close();			
			if(tcpsock!=null)
				tcpsock.close();
		}catch(IOException ioe){
			System.out.println("Unable to close i/o streams and sockets in StateManager.callEnded().");
			return;
		}
		
		//wait until everything's closed and cleaned up
		while(!tcpreceiver.isClosed()){}
		
		//end current threads and objects
		pool.shutdownNow();
		while(!pool.isShutdown()){} //wait until all threads shut down
		System.out.println("Pool shutdown complete.");
		
		//trying to close the udp socket causes issues
		/*
		datagramSocket.close();
		try{
			datagramSocket = new DatagramSocket(VIPER_PORT);
		} catch (SocketException e) {
			System.out.println("Unable to initialize UDP port in StateManager.callEnded().");
		}
		*/
		
		//re-init and clear all objects
		tcpsock = null;
		dos = null;
		dis = null;
		pool = Executors.newCachedThreadPool();
		udpreceiver = new UDPReceiver();
		pool.execute(udpreceiver); //start UDPReceiver
		tcpreceiver = new TCPReceiver(null);
		pool.execute(tcpreceiver); //start TCPReceiver
		ViperGUI.INSTANCE.getInCallPanel().clearGraph();
	}
	//--------------------------------------------------------------
	//----------------------exiting the program---------------------
	/**
	 * called by ViperGUI -  when user closes the window,
	 * make sure the user gets logged out
	 */
	public void exit() {
		if(state!=State.LOGGED_OUT)
			logout();
	}
	//----------------------------------------------------------------
}
